/**
 * 
 */
package jmine.tec.report.impl;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import jmine.tec.report.ColumnFactory;
import jmine.tec.report.CurriedDataColumn;
import jmine.tec.report.DataColumn;
import jmine.tec.report.ExecutableDataColumn;
import jmine.tec.report.ExecutableDataColumnContainer;
import jmine.tec.report.metadata.ColumnMetaData;
import jmine.tec.report.metadata.ParameterDefinition;
import jmine.tec.report.metadata.ParameterType;
import jmine.tec.report.metadata.impl.UnmodifiableColumnMetaData;

/**
 * @author takeshi
 * @version 29/08/2007
 */
public class SimpleCurriedDataColumn implements Serializable, DataColumn, CurriedDataColumn {
    private static final long serialVersionUID = 2363545381746539174L;

    private final DataColumn delegate;

    private final Map<String, Object> fixedParameters;

    private final Map<String, ParameterDefinition> curried = new HashMap<String, ParameterDefinition>();

    /**
     * C'tor
     * 
     * @param delegate the column to curry
     * @param fixedParameters the curried parameters
     */
    public SimpleCurriedDataColumn(final DataColumn delegate, final Map<String, Object> fixedParameters) {
        super();
        this.delegate = delegate;
        this.fixedParameters = new HashMap<String, Object>(fixedParameters.size());
        final Set<Entry<String, Object>> entrySet = fixedParameters.entrySet();
        for (final Entry<String, Object> entry : entrySet) {
            this.fixParameter(entry.getKey(), entry.getValue());
        }
    }

    /**
     * @param obj the parameter to compare
     * @return boolean
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (this.getClass() != obj.getClass()) {
            return false;
        }
        final SimpleCurriedDataColumn other = (SimpleCurriedDataColumn) obj;
        if (this.delegate == null) {
            if (other.delegate != null) {
                return false;
            }
        } else if (!this.delegate.equals(other.delegate)) {
            return false;
        }
        if (this.fixedParameters == null) {
            if (other.fixedParameters != null) {
                return false;
            }
        } else if (!this.fixedParameters.equals(other.fixedParameters)) {
            return false;
        }
        return true;
    }

    /**
     * Fixes a parameter from this column. Throws {@link IllegalArgumentException} if the parameter cannot be found or if the value is
     * incompatible with the parameter type.
     * <p>
     * Note that <code>null</code> is allowed as a parameter value and will override parameters when executing
     * 
     * @param parameterName the paramter name
     * @param value the value to fix
     */
    public final void fixParameter(final String parameterName, final Object value) {
        final ColumnMetaData meta = this.delegate.getMetaData();
        final ParameterType type = meta.getParameterType(parameterName);
        if (type != null) {
            if ((value == null) || type.allowedType(value.getClass())) {
                this.fixedParameters.put(parameterName, value);
                this.curried.put(parameterName, meta.getParameterDefinition(parameterName));
            } else {
                throw new IllegalArgumentException("Incompatible paramter type. Required:     " + type + " but was: "
                        + value.getClass());
            }
        } else {
            throw new IllegalArgumentException("No such paramter: " + parameterName);
        }
    }

    /**
     * @param parameterKey String
     * @return Object
     * @see br.com.maps.util.report.DataColumn#getDefaultParameterValue(java.lang.String)
     */
    public final Object getDefaultParameterValue(final String parameterKey) {
        if (this.fixedParameters.containsKey(parameterKey)) {
            return this.fixedParameters.get(parameterKey);
        }
        return this.delegate.getDefaultParameterValue(parameterKey);
    }

    /**
     * @return the key
     * @see br.com.maps.util.report.DataColumn#getKey()
     */
    public final String getKey() {
        return this.delegate.getKey();
    }

    /**
     * Filters the metadata from the curried column, excluding fixed parameters
     * 
     * @return {@link ColumnMetaData}
     * @see br.com.maps.util.report.DataColumn#getMetaData()
     */
    public final ColumnMetaData getMetaData() {
        if (this.fixedParameters.isEmpty()) { // shortcut
            return this.delegate.getMetaData();
        }
        return this.filterMetaData(this.delegate.getMetaData());
    }

    /**
     * @return the name
     * @see br.com.maps.util.report.DataColumn#getName()
     */
    public final String getName() {
        return this.delegate.getName();
    }

    /**
     * Returns a view
     * 
     * @param params {@link Map}
     * @return {@link SimpleDataColumn}
     * @see br.com.maps.util.report.DataColumn#getView(java.util.Map)
     */
    public final SimpleDataColumn getView(final Map<String, Object> params) {
        final SimpleDataColumn view = this.delegate.getView(this.mergeParameters(params));
        view.setMetaData(this.getMetaData());
        return view;
    }

    /**
     * @return int
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((this.delegate == null) ? 0 : this.delegate.hashCode());
        result = prime * result + ((this.fixedParameters == null) ? 0 : this.fixedParameters.hashCode());
        return result;
    }

    /**
     * @return boolean
     * @see br.com.maps.util.report.DataColumn#isSortable()
     */
    public final boolean isSortable() {
        return this.delegate.isSortable();
    }

    /**
     * Filters the {@link ColumnMetaData}, removing fixed parameters
     * 
     * @param other {@link ColumnMetaData} to filter
     * @return {@link ColumnMetaData} filtered
     */
    protected final ColumnMetaData filterMetaData(final ColumnMetaData other) {
        final Map<String, ParameterDefinition> allParameterDefinitions =
                new HashMap<String, ParameterDefinition>(other.getAllParameterDefinitions());
        for (final String key : this.fixedParameters.keySet()) {
            allParameterDefinitions.remove(key);
        }
        return new UnmodifiableColumnMetaData(this.getKey(), this.getName(), other.getExampleDataCell(), other
                .getChartTypes(), allParameterDefinitions.values().toArray(
                new ParameterDefinition[allParameterDefinitions.size()]));
    }

    /**
     * Gets all the parameter definitions that are curried in this {@link DataColumn}
     * 
     * @return {@link Collection} of {@link ParameterDefinition}
     */
    public final Collection<ParameterDefinition> getCurriedParameterDefinitions() {
        return this.curried.values();
    }

    /**
     * Gets the name of the curried parameters
     * 
     * @return {@link Collection} of {@link String}
     */
    public final Collection<String> getCurriedParameterNames() {
        return this.curried.keySet();
    }

    /**
     * @return the delegate
     */
    protected final DataColumn getDelegate() {
        return this.delegate;
    }

    /**
     * @return the fixedParameters
     */
    protected final Map<String, Object> getFixedParameters() {
        return this.fixedParameters;
    }

    /**
     * @return the curried
     */
    protected final Map<String, ParameterDefinition> getCurried() {
        return this.curried;
    }

    /**
     * Returns a {@link Map} with all curried parameters. Modifications on this {@link Map} should not reflect back on this
     * CurriedDataColumn.
     * 
     * @return {@link Map} of {@link String} to {@link Object}
     */
    public final Map<String, Object> getAllCurriedParameters() {
        return new HashMap<String, Object>(this.fixedParameters);
    }

    /**
     * Merge parameters from configuration before execution. Replaces the parameters from the map with the fixed parameters on this
     * {@link CurriedExecutableDataColumn}.
     * 
     * @param map the map
     * @return {@link Map}
     */
    protected final Map<String, Object> mergeParameters(final Map<String, Object> map) {
        if (this.getFixedParameters().isEmpty()) {
            // no need to curry
            return map;
        }
        final Map<String, Object> merged =
                (map == null ? new HashMap<String, Object>() : new HashMap<String, Object>(map));
        merged.putAll(this.getFixedParameters());
        return merged;
    }

    /**
     * @param container o container
     * @return column
     */
    public List<ExecutableDataColumn> buildColumns(final ExecutableDataColumnContainer container) {
        if (this.delegate instanceof ColumnFactory) {
            List<ExecutableDataColumn> columns = ((ColumnFactory) this.delegate).buildColumns(container);
            List<ExecutableDataColumn> curriedColumns = new ArrayList<ExecutableDataColumn>(columns.size());
            for (ExecutableDataColumn executableDataColumn : columns) {
                curriedColumns.add(new CurriedExecutableDataColumn(executableDataColumn, this.fixedParameters));
            }
            return curriedColumns;
        } else {
            ExecutableDataColumn del = container.findExecutableDataColumn(this.getKey());
            ExecutableDataColumn c = new CurriedExecutableDataColumn(del, this.fixedParameters);
            return Collections.singletonList(c);
        }
    }
}
